﻿using ImageK.Gui;
using ImageK.IO;
using System;
using System.Collections.Generic;
using System.IO.Packaging;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using ImageK.Java;
using ImageK.Process;

namespace ImageK.IO
{
    /** Saves an ROI to a file or stream. RoiDecoder.java has a description of the file format.
        @see ij.io.RoiDecoder
        @see ij.plugin.RoiReader
    */
    public class RoiEncoder
    {
        public static int HEADER_SIZE = 64;
        static int HEADER2_SIZE = 64;
        static int VERSION = 228; // v1.52t (roi groups, scale stroke width)
        private string path;
        private BinaryWriter f;
        private int polygon = 0, rect = 1, oval = 2, line = 3, freeline = 4, polyline = 5, noRoi = 6, freehand = 7,
            traced = 8, angle = 9, point = 10;
        private byte[] data;
        private String roiName;
        private int roiNameSize;
        private String roiProps;
        private int roiPropsSize;
        private int countersSize;
        private int[] counters;


        /** Creates an RoiEncoder using the specified path. */
        public RoiEncoder(string path)
        {
            this.path = path;
        }

        /** Creates an RoiEncoder using the specified OutputStream. */
        public RoiEncoder(BinaryWriter f)
        {
            this.f = f;
        }

        /** Saves the specified ROI as a file, returning 'true' if successful. */
        public static bool save(Roi roi, string path)
        {
            RoiEncoder re = new RoiEncoder(path);
            try
            {
                re.write(roi);
            }
            catch (IOException e)
            {
                return false;
            }
            return true;
        }

        /** Save the Roi to the file of stream. */
        public void write(Roi roi)
        {
		    if (f!=null) {
			    write(roi, f);
            } else {
                FileStream fs = new FileStream(path, FileMode.Create);
                f = new BinaryWriter(fs);
                write(roi, f);
                f.Close();
		    }
	    }
	
	    /** Saves the specified ROI as a byte array. */
	    public static byte[] saveAsByteArray(Roi roi)
        {
             if (roi==null) return null;
             byte[] bytes = null;
             try
             {
                 using MemoryStream ot = new MemoryStream(4096);
                 using BinaryWriter bw = new BinaryWriter(ot);
                 RoiEncoder encoder = new RoiEncoder(bw);
                 encoder.write(roi);
                 bytes = ot.ToArray();
             }
             catch (IOException e)
             {
                 return null;
             }
             return bytes;
        }

        void write(Roi roi, BinaryWriter f)
        {
            Rectangle r = roi.getBounds();
		        if (r.Width>60000||r.Height>60000||r.X>60000||r.Y>60000)
			        roi.enableSubPixelResolution();
		        int roiType = roi.getType();
		        int type = rect;
		        int options = 0;
		        if (roi.getScaleStrokeWidth())
			        options |= RoiDecoder.SCALE_STROKE_WIDTH;
                roiName = roi.getName();
		        if (roiName!=null)
			        roiNameSize = roiName.Length*2;
		        else
			        roiNameSize = 0;

                roiProps = roi.getProperties();
		        if (roiProps!=null)
			        roiPropsSize = roiProps.Length*2;
		        else
			        roiPropsSize = 0;

		        switch (roiType) {
                    case Roi.POLYGON: type=polygon; break;
                    case Roi.FREEROI: type=freehand; break;
                    case Roi.TRACED_ROI: type=traced; break;
                    case Roi.OVAL: type=oval; break;
                    case Roi.LINE: type=line; break;
                    case Roi.POLYLINE: type=polyline; break;
                    case Roi.FREELINE: type=freeline; break;
                    case Roi.ANGLE: type=angle; break;
                    case Roi.COMPOSITE: type=rect; break; // shape array size (36-39) will be >0 to indicate composite type
                    case Roi.POINT: type=point; break;
                    default: type = rect; break;
                }

                if (roiType==Roi.COMPOSITE)
                {
                    saveShapeRoi(roi, type, f, options);
                    return;
                }

                int n = 0;
                int[] x = null, y = null;
                float[] xf = null, yf = null;
                int floatSize = 0;
                if (roi is PolygonRoi) {
                    PolygonRoi proi = (PolygonRoi)roi;
                    Polygon p = proi.getNonSplineCoordinates();
                    n = p.npoints;
                    x = p.xpoints;
                    y = p.ypoints;
                    if (roi.subPixelResolution())
                    {
                        FloatPolygon fp = null;
                        if (proi.isSplineFit())
                            fp = proi.getNonSplineFloatPolygon();
                        else
                            fp = roi.getFloatPolygon();
                        if (n==fp.npoints)
                        {
                            options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
                            if (roi.getDrawOffset())
                                options |= RoiDecoder.DRAW_OFFSET;
                            xf = fp.xpoints;
                            yf = fp.ypoints;
                            floatSize = n*8;
                        }
                    }
                }

                countersSize = 0;
                if (roi is PointRoi) {
                    counters = ((PointRoi)roi).getCounters();
                    if (counters!=null && counters.Length>=n)
                        countersSize = n*4;
                }

                data = new byte[HEADER_SIZE+HEADER2_SIZE+n*4+floatSize+roiNameSize+roiPropsSize+countersSize];
                data[0]=73; data[1]=111; data[2]=117; data[3]=116; // "Iout"
                putShort(RoiDecoder.VERSION_OFFSET, VERSION);
                data[RoiDecoder.TYPE] = (byte)type;
                putShort(RoiDecoder.TOP, r.Y);
                putShort(RoiDecoder.LEFT, r.X);
                putShort(RoiDecoder.BOTTOM, r.Y+r.Height);
                putShort(RoiDecoder.RIGHT, r.X+r.Width);
                if (roi.subPixelResolution() && (type==rect||type==oval))
                {
                    FloatPolygon p = null;
                    if (roi is OvalRoi)
				        p = ((OvalRoi)roi).getFloatPolygon4();

                    else
                    {
                        int d = roi.getCornerDiameter();
                        if (d>0)
                        {
                            roi.setCornerDiameter(0);
                            p = roi.getFloatPolygon();
                            roi.setCornerDiameter(d);
                        }
                        else
                            p = roi.getFloatPolygon();
                    }
                    if (p.npoints==4)
                    {
                        putFloat(RoiDecoder.XD, p.xpoints[0]);
                        putFloat(RoiDecoder.YD, p.ypoints[0]);
                        putFloat(RoiDecoder.WIDTHD, p.xpoints[1]-p.xpoints[0]);
                        putFloat(RoiDecoder.HEIGHTD, p.ypoints[2]-p.ypoints[1]);
                        options |= RoiDecoder.SUB_PIXEL_RESOLUTION;
                        putShort(RoiDecoder.OPTIONS, options);
                    }
                }
                if (n>65535 && type!=point)
                {
                    if (type==polygon || type==freehand || type==traced)
                    {
                        String name = roi.getName();
                        roi = new ShapeRoi(roi);
                        if (name!=null) roi.setName(name);
                        saveShapeRoi(roi, rect, f, options);
                        return;
                    }
                    IJ.beep();
                    IJ.log("Non-polygonal selections with more than 65k points cannot be saved.");
                    n = 65535;
                }
                if (type==point && n>65535)
                    putInt(RoiDecoder.SIZE, n);
                else
                    putShort(RoiDecoder.N_COORDINATES, n);
                putInt(RoiDecoder.POSITION, roi.getPosition());

                if (type==rect)
                {
                    int arcSize = roi.getCornerDiameter();
                    if (arcSize>0)
                        putShort(RoiDecoder.ROUNDED_RECT_ARC_SIZE, arcSize);
                }

                if (roi is Line) {
                    Line line = (Line)roi;
                    putFloat(RoiDecoder.X1, (float)line.x1d);
                    putFloat(RoiDecoder.Y1, (float)line.y1d);
                    putFloat(RoiDecoder.X2, (float)line.x2d);
                    putFloat(RoiDecoder.Y2, (float)line.y2d);
                    if (roi is Arrow) {
                        //todo:
                        // putShort(RoiDecoder.SUBTYPE, RoiDecoder.ARROW);
                        // if (((Arrow)roi).getDoubleHeaded())
                        //     options |= RoiDecoder.DOUBLE_HEADED;
                        // if (((Arrow)roi).getOutline())
                        //     options |= RoiDecoder.OUTLINE;
                        // putShort(RoiDecoder.OPTIONS, options);
                        // putByte(RoiDecoder.ARROW_STYLE, ((Arrow)roi).getStyle());
                        // putByte(RoiDecoder.ARROW_HEAD_SIZE, (int)((Arrow)roi).getHeadSize());
                    } else
                    {
                        if (roi.getDrawOffset())
                            options |= RoiDecoder.SUB_PIXEL_RESOLUTION+RoiDecoder.DRAW_OFFSET;
                    }
                }

                if (roi is PointRoi) {
                    PointRoi point = (PointRoi)roi;
                    putByte(RoiDecoder.POINT_TYPE, point.getPointType());
                    putShort(RoiDecoder.STROKE_WIDTH, point.getSize());
                    if (point.getShowLabels())
                        options |= RoiDecoder.SHOW_LABELS;
                    if (point.promptBeforeDeleting())
                        options |= RoiDecoder.PROMPT_BEFORE_DELETING;
                }

                if (roi is RotatedRectRoi || roi is EllipseRoi) {
                    //todo:
                    // double[] p = null;
                    // if (roi is RotatedRectRoi) {
                    //     putShort(RoiDecoder.SUBTYPE, RoiDecoder.ROTATED_RECT);
                    //     p = ((RotatedRectRoi)roi).getParams();
                    // } else
                    // {
                    //     putShort(RoiDecoder.SUBTYPE, RoiDecoder.ELLIPSE);
                    //     p = ((EllipseRoi)roi).getParams();
                    // }
                    // putFloat(RoiDecoder.X1, (float)p[0]);
                    // putFloat(RoiDecoder.Y1, (float)p[1]);
                    // putFloat(RoiDecoder.X2, (float)p[2]);
                    // putFloat(RoiDecoder.Y2, (float)p[3]);
                    // putFloat(RoiDecoder.FLOAT_PARAM, (float)p[4]);
                }

                // save stroke width, stroke color and fill color (1.43i or later)
                if (VERSION>=218)
                {
                    saveStrokeWidthAndColor(roi);
                    if ((roi is PolygonRoi) && ((PolygonRoi)roi).isSplineFit()) {
                        options |= RoiDecoder.SPLINE_FIT;
                        putShort(RoiDecoder.OPTIONS, options);
                    }
                }

                if (n==0 && roi is TextRoi)
			        saveTextRoi((TextRoi)roi);

                else if (n==0 && roi is ImageRoi)
			        options = saveImageRoi((ImageRoi)roi, options);

                else
                    putHeader2(roi, HEADER_SIZE+n*4+floatSize);

                if (n>0)
                {
                    int base1 = 64;
                    int base2 = base1+2*n;
                    for (int i = 0; i<n; i++)
                    {
                        putShort(base1+i*2, x[i]);
                        putShort(base2+i*2, y[i]);
                    }
                    if (xf!=null)
                    {
                        base1 = 64+4*n;
                        base2 = base1+4*n;
                        for (int i = 0; i<n; i++)
                        {
                            putFloat(base1+i*4, xf[i]);
                            putFloat(base2+i*4, yf[i]);
                        }
                    }
                }

                saveOverlayOptions(roi, options);
                f.Write(data);
            }

	    void saveStrokeWidthAndColor(Roi roi) {
            //todo:
            // BasicStroke stroke = roi.getStroke();
            // if (stroke!=null)
            //     putShort(RoiDecoder.STROKE_WIDTH, (int)stroke.getLineWidth());
            // Color strokeColor = roi.getStrokeColor();
            // if (strokeColor!=null)
            //     putInt(RoiDecoder.STROKE_COLOR, strokeColor.getRGB());
            // Color fillColor = roi.getFillColor();
            // if (fillColor!=null)
            //     putInt(RoiDecoder.FILL_COLOR, fillColor.getRGB());
        }

	    void saveShapeRoi(Roi roi, int type, BinaryWriter bout, int options)
        {
            float[] shapeArray = ((ShapeRoi)roi).getShapeAsArray();
            if (shapeArray==null) return;
            // BinaryWriter bout = new BinaryWriter(f);
            Rectangle r = roi.getBounds();
            data  = new byte[HEADER_SIZE+HEADER2_SIZE+shapeArray.Length*4+roiNameSize+roiPropsSize];
            data[0]=73; data[1]=111; data[2]=117; data[3]=116; // "Iout"

            putShort(RoiDecoder.VERSION_OFFSET, VERSION);
            data[RoiDecoder.TYPE] = (byte)type;
            putShort(RoiDecoder.TOP, r.Y);
            putShort(RoiDecoder.LEFT, r.X);
            putShort(RoiDecoder.BOTTOM, r.Y+r.Height);
            putShort(RoiDecoder.RIGHT, r.X+r.Width);
            putInt(RoiDecoder.POSITION, roi.getPosition());
            //putShort(16, n);
            putInt(36, shapeArray.Length); // non-zero segment count indicate composite type
            if (VERSION>=218) saveStrokeWidthAndColor(roi);
            saveOverlayOptions(roi, options);

            // handle the actual data: data are stored segment-wise, i.e.,
            // the type of the segment followed by 0-6 control point coordinates.
            int bas = 64;
            for (int i = 0; i<shapeArray.Length; i++)
            {
                putFloat(bas, shapeArray[i]);
                bas += 4;
            }
            int hdr2Offset = HEADER_SIZE+shapeArray.Length*4;
            //ij.IJ.log("saveShapeRoi: "+HEADER_SIZE+"  "+shapeArray.length);
            putHeader2(roi, hdr2Offset);
            bout.Write(data, 0, data.Length);
            bout.Flush();
        }

	    void saveOverlayOptions(Roi roi, int options) {
            //todo:
            // Overlay proto = roi.getPrototypeOverlay();
            // if (proto.getDrawLabels())
            //     options |= RoiDecoder.OVERLAY_LABELS;
            // if (proto.getDrawNames())
            //     options |= RoiDecoder.OVERLAY_NAMES;
            // if (proto.getDrawBackgrounds())
            //     options |= RoiDecoder.OVERLAY_BACKGROUNDS;
            // Font font = proto.getLabelFont();
            // if (font!=null && font.getStyle()==Font.BOLD)
            //     options |= RoiDecoder.OVERLAY_BOLD;
            // if (proto.scalableLabels())
            //     options |= RoiDecoder.SCALE_LABELS;
            // putShort(RoiDecoder.OPTIONS, options);
        }

	    void saveTextRoi(TextRoi roi) {
            // Font font = roi.getCurrentFont();
            // String fontName = font.getName();
            // int size = font.getSize();
            // int drawStringMode = roi.getDrawStringMode() ? 1024 : 0;
            // int style = font.getStyle() + roi.getJustification()*256+drawStringMode;
            // String text = roi.getText();
            // float angle = (float)roi.getAngle();
            // int angleLength = 4;
            // int fontNameLength = fontName.length();
            // int textLength = text.length();
            // int textRoiDataLength = 16+fontNameLength*2+textLength*2 + angleLength;
            // byte[] data2 = new byte[HEADER_SIZE+HEADER2_SIZE+textRoiDataLength+roiNameSize+roiPropsSize];
            // System.arraycopy(data, 0, data2, 0, HEADER_SIZE);
            // data = data2;
            // putShort(RoiDecoder.SUBTYPE, RoiDecoder.TEXT);
            // putInt(HEADER_SIZE, size);
            // putInt(HEADER_SIZE+4, style);
            // putInt(HEADER_SIZE+8, fontNameLength);
            // putInt(HEADER_SIZE+12, textLength);
            // for (int i = 0; i<fontNameLength; i++)
            //     putShort(HEADER_SIZE+16+i*2, fontName.charAt(i));
            // for (int i = 0; i<textLength; i++)
            //     putShort(HEADER_SIZE+16+fontNameLength*2+i*2, text.charAt(i));
            // int hdr2Offset = HEADER_SIZE+textRoiDataLength;
            // //ij.IJ.log("saveTextRoi: "+HEADER_SIZE+"  "+textRoiDataLength+"  "+fontNameLength+"  "+textLength);
            // putFloat(hdr2Offset-angleLength, angle);
            // putHeader2(roi, hdr2Offset);
        }


        private int saveImageRoi(ImageRoi roi, int options)
        {
            // byte[] bytes = roi.getSerializedImage();
            // int imageSize = bytes.length;
            // byte[] data2 = new byte[HEADER_SIZE+HEADER2_SIZE+imageSize+roiNameSize+roiPropsSize];
            // System.arraycopy(data, 0, data2, 0, HEADER_SIZE);
            // data = data2;
            // putShort(RoiDecoder.SUBTYPE, RoiDecoder.IMAGE);
            // for (int i = 0; i<imageSize; i++)
            //     putByte(HEADER_SIZE+i, bytes[i]&255);
            // int hdr2Offset = HEADER_SIZE+imageSize;
            // double opacity = roi.getOpacity();
            // putByte(hdr2Offset+RoiDecoder.IMAGE_OPACITY, (int)(opacity*255.0));
            // putInt(hdr2Offset+RoiDecoder.IMAGE_SIZE, imageSize);
            // if (roi.getZeroTransparent())
            //     options |= RoiDecoder.ZERO_TRANSPARENT;
            // putHeader2(roi, hdr2Offset);
            // return options;

            return 0;
        }

        void putHeader2(Roi roi, int hdr2Offset)
        {
            //ij.IJ.log("putHeader2: "+hdr2Offset+" "+roiNameSize+"  "+roiName);
            putInt(RoiDecoder.HEADER2_OFFSET, hdr2Offset);
            putInt(hdr2Offset+RoiDecoder.C_POSITION, roi.getCPosition());
            putInt(hdr2Offset+RoiDecoder.Z_POSITION, roi.hasHyperStackPosition() ? roi.getZPosition() : 0);
            putInt(hdr2Offset+RoiDecoder.T_POSITION, roi.getTPosition());
            //todo:
            // Overlay proto = roi.getPrototypeOverlay();
            // Color overlayLabelColor = proto.getLabelColor();
            // if (overlayLabelColor!=null)
            //     putInt(hdr2Offset+RoiDecoder.OVERLAY_LABEL_COLOR, overlayLabelColor.getRGB());
            // Font font = proto.getLabelFont();
            // if (font!=null)
            //     putShort(hdr2Offset+RoiDecoder.OVERLAY_FONT_SIZE, (int)font.Size);
            if (roiNameSize>0)
                putName(roi, hdr2Offset);
            double strokeWidth = roi.getStrokeWidth();
            if (roi.getStroke()==null)
                strokeWidth = 0.0;
            putFloat(hdr2Offset+RoiDecoder.FLOAT_STROKE_WIDTH, (float)strokeWidth);
            if (roiPropsSize>0)
                putProps(roi, hdr2Offset);
            if (countersSize>0)
                putPointCounters(roi, hdr2Offset);
            putByte(hdr2Offset+RoiDecoder.GROUP, roi.getGroup());
        }

        void putName(Roi roi, int hdr2Offset)
        {
            int offset = hdr2Offset+HEADER2_SIZE;
            int nameLength = roiNameSize/2;
            putInt(hdr2Offset+RoiDecoder.NAME_OFFSET, offset);
            putInt(hdr2Offset+RoiDecoder.NAME_LENGTH, nameLength);
            for (int i = 0; i<nameLength; i++)
                putShort(offset+i*2, roiName[i]);
        }

        void putProps(Roi roi, int hdr2Offset)
        {
            int offset = hdr2Offset+HEADER2_SIZE+roiNameSize;
            int roiPropsLength = roiPropsSize/2;
            putInt(hdr2Offset+RoiDecoder.ROI_PROPS_OFFSET, offset);
            putInt(hdr2Offset+RoiDecoder.ROI_PROPS_LENGTH, roiPropsLength);
            for (int i = 0; i<roiPropsLength; i++)
                putShort(offset+i*2, roiProps[i]);
        }

        void putPointCounters(Roi roi, int hdr2Offset)
        {
            int offset = hdr2Offset+HEADER2_SIZE+roiNameSize+roiPropsSize;
            putInt(hdr2Offset+RoiDecoder.COUNTERS_OFFSET, offset);
            for (int i = 0; i<countersSize/4; i++)
                putInt(offset+i*4, counters[i]);
            countersSize = 0;
        }

        void putByte(int bas, int v)
        {
            data[bas] = (byte)v;
        }

        void putShort(int bas, int v)
        {
            data[bas] = (byte)(v>>8);
            data[bas+1] = (byte)v;
        }

        void putFloat(int bas, float v)
        {
            // int tmp = Float.floatToIntBits(v);
            int tmp = BitConverter.SingleToInt32Bits(v);
            data[bas]   = (byte)(tmp>>24);
            data[bas+1] = (byte)(tmp>>16);
            data[bas+2] = (byte)(tmp>>8);
            data[bas+3] = (byte)tmp;
        }

        void putInt(int bas, int i)
        {
            data[bas]   = (byte)(i>>24);
            data[bas+1] = (byte)(i>>16);
            data[bas+2] = (byte)(i>>8);
            data[bas+3] = (byte)i;
        }
	
	
    }
}
